layout.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. 'use client'
  2. import type { FC } from 'react'
  3. import { useUnmount } from 'ahooks'
  4. import React, { useCallback, useEffect, useState } from 'react'
  5. import { usePathname, useRouter } from 'next/navigation'
  6. import {
  7. RiDashboard2Fill,
  8. RiDashboard2Line,
  9. RiFileList3Fill,
  10. RiFileList3Line,
  11. RiTerminalBoxFill,
  12. RiTerminalBoxLine,
  13. RiTerminalWindowFill,
  14. RiTerminalWindowLine,
  15. } from '@remixicon/react'
  16. import { useTranslation } from 'react-i18next'
  17. import { useShallow } from 'zustand/react/shallow'
  18. import { useContextSelector } from 'use-context-selector'
  19. import s from './style.module.css'
  20. import cn from '@/utils/classnames'
  21. import { useStore } from '@/app/components/app/store'
  22. import AppSideBar from '@/app/components/app-sidebar'
  23. import type { NavIcon } from '@/app/components/app-sidebar/navLink'
  24. import { fetchAppDetail, fetchAppSSO } from '@/service/apps'
  25. import AppContext, { useAppContext } from '@/context/app-context'
  26. import Loading from '@/app/components/base/loading'
  27. import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
  28. import type { App } from '@/types/app'
  29. export type IAppDetailLayoutProps = {
  30. children: React.ReactNode
  31. params: { appId: string }
  32. }
  33. const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
  34. const {
  35. children,
  36. params: { appId }, // get appId in path
  37. } = props
  38. const { t } = useTranslation()
  39. const router = useRouter()
  40. const pathname = usePathname()
  41. const media = useBreakpoints()
  42. const isMobile = media === MediaType.mobile
  43. const { isCurrentWorkspaceEditor, isLoadingCurrentWorkspace } = useAppContext()
  44. const { appDetail, setAppDetail, setAppSiderbarExpand } = useStore(useShallow(state => ({
  45. appDetail: state.appDetail,
  46. setAppDetail: state.setAppDetail,
  47. setAppSiderbarExpand: state.setAppSiderbarExpand,
  48. })))
  49. const [isLoadingAppDetail, setIsLoadingAppDetail] = useState(false)
  50. const [appDetailRes, setAppDetailRes] = useState<App | null>(null)
  51. const [navigation, setNavigation] = useState<Array<{
  52. name: string
  53. href: string
  54. icon: NavIcon
  55. selectedIcon: NavIcon
  56. }>>([])
  57. const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
  58. const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => {
  59. const navs = [
  60. ...(isCurrentWorkspaceEditor
  61. ? [{
  62. name: t('common.appMenus.promptEng'),
  63. href: `/app/${appId}/${(mode === 'workflow' || mode === 'advanced-chat') ? 'workflow' : 'configuration'}`,
  64. icon: RiTerminalWindowLine,
  65. selectedIcon: RiTerminalWindowFill,
  66. }]
  67. : []
  68. ),
  69. {
  70. name: t('common.appMenus.apiAccess'),
  71. href: `/app/${appId}/develop`,
  72. icon: RiTerminalBoxLine,
  73. selectedIcon: RiTerminalBoxFill,
  74. },
  75. ...(isCurrentWorkspaceEditor
  76. ? [{
  77. name: mode !== 'workflow'
  78. ? t('common.appMenus.logAndAnn')
  79. : t('common.appMenus.logs'),
  80. href: `/app/${appId}/logs`,
  81. icon: RiFileList3Line,
  82. selectedIcon: RiFileList3Fill,
  83. }]
  84. : []
  85. ),
  86. {
  87. name: t('common.appMenus.overview'),
  88. href: `/app/${appId}/overview`,
  89. icon: RiDashboard2Line,
  90. selectedIcon: RiDashboard2Fill,
  91. },
  92. ]
  93. return navs
  94. }, [t])
  95. useEffect(() => {
  96. if (appDetail) {
  97. document.title = `${(appDetail.name || 'App')} - Dify`
  98. const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
  99. const mode = isMobile ? 'collapse' : 'expand'
  100. setAppSiderbarExpand(isMobile ? mode : localeMode)
  101. // TODO: consider screen size and mode
  102. // if ((appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (pathname).endsWith('workflow'))
  103. // setAppSiderbarExpand('collapse')
  104. }
  105. }, [appDetail, isMobile])
  106. useEffect(() => {
  107. setAppDetail()
  108. setIsLoadingAppDetail(true)
  109. fetchAppDetail({ url: '/apps', id: appId }).then((res) => {
  110. setAppDetailRes(res)
  111. }).catch((e: any) => {
  112. if (e.status === 404)
  113. router.replace('/apps')
  114. }).finally(() => {
  115. setIsLoadingAppDetail(false)
  116. })
  117. }, [appId, router, setAppDetail])
  118. useEffect(() => {
  119. if (!appDetailRes || isLoadingCurrentWorkspace || isLoadingAppDetail)
  120. return
  121. const res = appDetailRes
  122. // redirection
  123. const canIEditApp = isCurrentWorkspaceEditor
  124. if (!canIEditApp && (pathname.endsWith('configuration') || pathname.endsWith('workflow') || pathname.endsWith('logs'))) {
  125. router.replace(`/app/${appId}/overview`)
  126. return
  127. }
  128. if ((res.mode === 'workflow' || res.mode === 'advanced-chat') && (pathname).endsWith('configuration')) {
  129. router.replace(`/app/${appId}/workflow`)
  130. }
  131. else if ((res.mode !== 'workflow' && res.mode !== 'advanced-chat') && (pathname).endsWith('workflow')) {
  132. router.replace(`/app/${appId}/configuration`)
  133. }
  134. else {
  135. setAppDetail({ ...res, enable_sso: false })
  136. setNavigation(getNavigations(appId, isCurrentWorkspaceEditor, res.mode))
  137. if (systemFeatures.enable_web_sso_switch_component && canIEditApp) {
  138. fetchAppSSO({ appId }).then((ssoRes) => {
  139. setAppDetail({ ...res, enable_sso: ssoRes.enabled })
  140. })
  141. }
  142. }
  143. }, [appDetailRes, appId, getNavigations, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace, pathname, router, setAppDetail, systemFeatures.enable_web_sso_switch_component])
  144. useUnmount(() => {
  145. setAppDetail()
  146. })
  147. if (!appDetail) {
  148. return (
  149. <div className='flex h-full items-center justify-center bg-background-body'>
  150. <Loading />
  151. </div>
  152. )
  153. }
  154. return (
  155. <div className={cn(s.app, 'flex', 'overflow-hidden')}>
  156. {appDetail && (
  157. <AppSideBar title={appDetail.name} icon={appDetail.icon} icon_background={appDetail.icon_background} desc={appDetail.mode} navigation={navigation} />
  158. )}
  159. <div className="bg-components-panel-bg grow overflow-hidden">
  160. {children}
  161. </div>
  162. </div>
  163. )
  164. }
  165. export default React.memo(AppDetailLayout)